const path = require('path');
const fs = require('fs').promises;
const { isEqualBlock, fileToRequest, resolveRelativePath } = require('../../utils');
const LRUCache = require('lru-cache');
const { codegenCss } = require('./serverPluginStyle');
const hash_sum = require('hash-sum');
const importParse = require('es-module-lexer').parse;

const vueCache = new LRUCache({
  max: 65535
})

const srcImportMap = new Map();

function resolveImports(descriptor, ctx, root) {
  const imports = importParse(descriptor.script.content);
  if (imports[0] && imports[0].length > 0) {
    for (let i = 0; i < imports[0].length; i++) {
      if (imports[0][i].n && imports[0][i].n.endsWith('.js')) {
        let importFromPath = imports[0][i].n;
        let currentFilePath = ctx.url.toString().split('?')[0];
        let srcImportKey = resolveRelativePath(importFromPath, currentFilePath);
        srcImportMap.set(srcImportKey, currentFilePath);
      }
    }
  }
}

function vuePlugin(context) {
  const { app, root, watcher } = context;

  // 解析模板
  const { compileTemplate, parse } = require(path.resolve(root, 'node_modules', '@vue/compiler-sfc/dist/compiler-sfc.cjs.js'));

  app.use(async (ctx, next) => {
    if (!ctx.path.endsWith('.vue')) {
      return next();
    }

    const filePath = path.join(root, ctx.path);

    const content = await fs.readFile(filePath, 'utf-8');

    const { descriptor } = parse(content, {
      filename: filePath,
      sourceMap: true
    });
    if (!ctx.query.type) {
      let code = '';
      let map;
      // console.log('descriptor.script.src', descriptor)
      // if (descriptor.script && descriptor.script.src) {
      //   srcImportMap.set(filePath, ctx.url)
      // }
      if (descriptor.script) {
        let content = descriptor.script.content;
        code += content.replace(/((?:^|\n|;)\s*)export default/, '$1const __script=');
        map = descriptor.script.map;
        resolveImports(descriptor, ctx, root);
      }
      if (descriptor.template) {
        const requestPath = ctx.path + '?type=template';
        code += `\nimport { render as __render } from "${requestPath}"`;
        code += '\n__script.render = __render';
      }
      if (descriptor.styles) {
        descriptor.styles.forEach((s, i) => {
          const styleRequest = ctx.path + `?type=style&index=${i}`
          code += `\nimport ${JSON.stringify(styleRequest)}`
        })
      }
      code += `\n__script.__hmrId = ${JSON.stringify(ctx.path)}`
      code += `\ntypeof __VUE_HMR_RUNTIME__ !== 'undefined' && __VUE_HMR_RUNTIME__.createRecord(__script.__hmrId, __script)`
      code += `\n__script.__file = ${JSON.stringify(filePath)}`
      code += '\nexport default __script';
      ctx.type = 'js';
      ctx.body = code;

      const result = {
        code,
        map,
        bindings: descriptor.script ? descriptor.script.bindings : undefined
      }

      let cached = vueCache.get(filePath);
      cached = cached || { styles: [], customs: [] };
      cached.script = result;
      cached.descriptor = descriptor;

      vueCache.set(filePath, cached)
    }

    if (ctx.query.type === 'template') {
      ctx.type = 'js';
      let content = descriptor.template.content;
      let { code } = compileTemplate({ source: content });
      ctx.body = code;
    }

    if (ctx.query.type === 'style') {
      const index = Number(ctx.query.index)
      const styleBlock = descriptor.styles[index]
      const id = hash_sum(ctx.path);
      ctx.type = 'js';
      ctx.body = codegenCss(`${id}-${index}`, styleBlock.content);
      return ctx;
    }
  })

  const handleVueReload = (watcher.handleVueReload = async (
    filePath,
    timestamp = Date.now()
  ) => {
    const publicPath = fileToRequest(filePath, root);

    const cacheEntry = vueCache.get(filePath);

    const { send } = watcher;

    let cached = vueCache.get(filePath);
    const content = await fs.readFile(filePath, 'utf-8');
    const { descriptor } = parse(content);

    if (!descriptor) {
      // read failed
      return
    }

    const prevDescriptor = cacheEntry && cacheEntry.descriptor;

    let needRerender = false

    const sendReload = () => {
      send({
        type: 'vue-reload',
        path: publicPath,
        changeSrcPath: publicPath,
        timestamp
      })
      console.log(
        `[vite:hmr] ` +
        `${path.relative(root, publicPath)} updated. (reload)`
      )
    }

    if (
      !isEqualBlock(descriptor.script, prevDescriptor.script)
    ) {
      return sendReload()
    }

    let didUpdateStyle = false
    const styleId = hash_sum(publicPath)
    const prevStyles = prevDescriptor.styles || []
    const nextStyles = descriptor.styles || []

    nextStyles.forEach((_, i) => {
      if (!prevStyles[i] || !isEqualBlock(prevStyles[i], nextStyles[i])) {
        didUpdateStyle = true
        const path = `${publicPath}?type=style&index=${i}`
        send({
          type: 'style-update',
          path,
          changeSrcPath: path,
          timestamp
        })
      }
    })

    prevStyles.slice(nextStyles.length).forEach((_, i) => {
      didUpdateStyle = true
      send({
        type: 'style-remove',
        path: publicPath,
        id: `${styleId}-${i + nextStyles.length}`
      })
    })

    if (!isEqualBlock(descriptor.template, prevDescriptor.template)) {
      needRerender = true
    }

    if (needRerender) {
      send({
        type: 'vue-rerender',
        path: publicPath,
        changeSrcPath: publicPath,
        timestamp
      })
    }

    cached = cached || { styles: [], customs: [] };
    cached.descriptor = descriptor;
    vueCache.set(filePath, cached);

    let updateType = []
    if (needRerender) {
      updateType.push(`template`)
    }
    if (didUpdateStyle) {
      updateType.push(`style`)
    }
    if (updateType.length) {
      console.log(
        `[vite:hmr] ` +
        `${path.relative(root, filePath)} updated. (${updateType.join(
          ' & '
        )})`
      )
    }
  })

  watcher.on('change', (file) => {
    if (file.endsWith('.vue')) {
      handleVueReload(file)
    }
  })
}

module.exports = {
  vuePlugin,
  srcImportMap,
  vueCache
}